﻿using Daemon.Common;
using Daemon.Common.Cache;
using Daemon.Data.Infrastructure.Auth;
using Daemon.Data.Substructure.Const;
using Daemon.Data.Substructure.Enums;
using Daemon.Data.Substructure.Helpers;
using Daemon.Middleware;
using Daemon.Model;
using Daemon.Repository.Contract;
using Daemon.Repository.EF;
using Daemon.Repository.EF.Executor;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.IO;
using System.Linq;
using System.Net.Mail;
using System.Net.Mime;
using System.Security.Claims;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Security.Cryptography;

namespace DaemonAPI.Helpers
{
    internal static class TokenHelper
    {
        private const string TokenIssuer = "self";
        private const string TokenKey = "Transfinder";
        private const string SupportTokenKey = "SupportToken";
        private const int TokenRetryPauseInSeconds = 5;

        internal static string CreateToken(string userName, string prefix, BaseAuthUser userEntity, string supportToken = "", Claim[] extraClaims = null)
        {
            Claim[] claims = new Claim[0];
            if (MFAHelper.IsEnableEmailAuth(userEntity) && UseMultifactorAuth(prefix))
            {
                claims = new[] { new Claim("Amr", "pwd,mfa") };
            }

            if (extraClaims != null)
            {
                claims = claims.Concat(extraClaims).ToArray();
            }

            return CreateTokenCore(userName, prefix, claims.Concat(BuildClaims(userEntity)).ToArray(), supportToken);
        }

        internal static string CreateRefreshToken()
        {
            var rnBytes = new byte[32];
            using var rng = RandomNumberGenerator.Create();
            rng.GetBytes(rnBytes);
            var token = Convert.ToBase64String(rnBytes);

            var options = new DistributedCacheEntryOptions();
            options.SetAbsoluteExpiration(GetExpiration());
            return token;
        }


        internal static string GetRefreshTokenKey(string userId, string refreshTokenId)
        {
            if (string.IsNullOrEmpty(userId)) throw new ArgumentNullException(nameof(userId));
            if (string.IsNullOrEmpty(refreshTokenId)) throw new ArgumentNullException(nameof(refreshTokenId));

            return $"{userId}:{refreshTokenId}";
        }

        internal static bool UseMultifactorAuth(string prefix)
        {
            return SystemConst.ENABLE_MULTIFACTOR_PREFIXS.Any(x => x.Equals(prefix, StringComparison.CurrentCultureIgnoreCase));
        }

        internal static bool SendMFACodeWithEmail(string emailAddress, string code, string prefix)
        {
            List<Attachment> attachments = new List<Attachment>();
            string appLogoName,
                appLogo,
                logoHtml = @"<tr>
								<td>
									<img src='cid:{0}'>
								</td>
							</tr>";

            switch (prefix)
            {
                case SystemConst.SERVICEFINDER_PREFIX:
                    appLogo = SystemConst.SERVICEFINDER_LOGO_BASE64;
                    appLogoName = "ServicefinderLogo";
                    break;
                default:
                    {
                        appLogo = SystemConst.ROUTEFINDER_LOGO_BASE64;
                        appLogoName = "PLUSLogo";
                        break;
                    }
            }

            byte[] bytes = Convert.FromBase64String(appLogo);
            bool result = false;

            using (MemoryStream ms = new MemoryStream(bytes))
            {
                var logoAttachment = new Attachment(ms, $"{appLogoName}.jpg", MediaTypeNames.Image.Jpeg);
                logoAttachment.ContentId = $"{appLogoName}";
                logoAttachment.ContentDisposition.Inline = true;
                attachments.Add(logoAttachment);
                var availableExpireTime = SystemConst.SERVICEFINDER_PREFIX.Equals(prefix) ? 10 : 5;

                var body = $@"<table style='width:100%;text-align:center;'>
							{string.Format(logoHtml, appLogoName)}
							<tr>
								<td>
									<p style='font-weight: 700;font-size:18px'>Your Security Code is:</p>
								</td>
							</tr>
							<tr>
								<td>
									<h1>{code}</h1>
								</td>
							</tr>
							<tr>
								<td>
									<p style='font-size:18px'><i>Note: it will only be active for {availableExpireTime} minutes.</i></p>
								</td>
							</tr>
						</table>";
                result = SESUtil.SendEmail(emailAddress, "Login Security Code", body, true, attachments);
            }

            return result;
        }

        private static string CreateTokenCore(string userName, string prefix, Claim[] extraClaims, string supportToken, bool isSaveToken = true)
        {
            var isSupport = SupportModeHelper.IsSupportModeUser(userName);
            var httpContextAccessor = ServiceLocator.Resolve<IHttpContextAccessor>();
            var configuration = ServiceLocator.Resolve<IConfiguration>();
            JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
            SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Secret"]));

            DateTime expiresAt = GetExpiration();

            var identity = new ClaimsIdentity(JwtBearerDefaults.AuthenticationScheme);

            IEnumerable<Claim> claims = new Claim[] {
                new Claim("Name", userName),
                new Claim("CreateTime", DateTime.Now.ToString()),
                new Claim("Prefix", prefix),
                new Claim(ClaimTypes.Expiration,expiresAt.ToString())
            };

            if (isSupport)
            {
                var supportClaims = new[] { new Claim(SupportTokenKey, supportToken) };
                claims = claims.Concat(supportClaims).ToArray();
            }

            identity.AddClaims(claims);
            if (extraClaims != null)
            {
                identity.AddClaims(extraClaims);
            }

            httpContextAccessor.HttpContext.SignInAsync(JwtBearerDefaults.AuthenticationScheme, new ClaimsPrincipal(identity));

            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = identity,
                Issuer = configuration["Jwt:Issuer"],
                Audience = configuration["Jwt:Audience"],
                SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
            };

            var token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
            var tokenString = tokenHandler.WriteToken(token);
            var tokenKey = GetTokenKey(userName, prefix);
            var tokenInfo = new TokenInfo() { TokenKey = tokenKey, TokenValue = tokenString, Expiration = expiresAt };
            if (isSaveToken)
            {
                SaveToken(isSupport, tokenInfo);
            }

            return tokenString;
        }

        private static void SaveToken(bool isSupport, TokenInfo tokenInfo)
        {
            var tokenRepository = ServiceLocator.Resolve<ITokenInfoRepository>();
            if (isSupport)
            {
                var supportToken = GetClaimValueFromToken(tokenInfo.TokenValue, SupportTokenKey);
                var existTokenInfo = tokenRepository.FindTokenInfoListByKey(tokenInfo.TokenKey)
                    .FirstOrDefault(r => GetClaimValueFromToken(r.TokenValue, SupportTokenKey) == supportToken);
                tokenRepository.Save(existTokenInfo?.Id, tokenInfo);
            }
            else
            {
                tokenRepository.Save(tokenInfo);
            }
        }

        private static List<Claim> BuildClaims(BaseAuthUser userEntity)
        {
            List<Claim> claims = new List<Claim>();

            switch (userEntity.UserType)
            {
                case "ADSecurity":
                    claims.Add(new Claim("ADSecurity", "True"));
                    claims.Add(new Claim("UserType", "ADSecurity"));
                    break;
                case "SAML":
                    claims.Add(new Claim("UserType", "SAML"));
                    break;
                case "WX":
                    claims.Add(new Claim("Id", userEntity.Id.ToString()));
                    claims.Add(new Claim("Name", userEntity.UserName.ToString()));
                    claims.Add(new Claim("UserType", "WX"));
                    break;
                default:
                    claims.Add(new Claim("Id", userEntity.Id.ToString()));
                    claims.Add(new Claim("UserType", "TFSecurity"));
                    break;
            }

            return claims;
        }

        private static DateTime GetExpiration()
        {
            return DateTime.Now.AddMinutes(GetExpirationMinutes());
        }

        private static string GetClaimValueFromToken(string token, string claimKey)
        {
            if (string.IsNullOrEmpty(token))
            {
                return null;
            }

            try
            {
                // Since the token should have been validated at the begining of the Request pipeline, we can use ReadToken (without signature validation) to save descryption cost
                var claimsDict = GetAllClaims(token).ToDictionary(c => c.Type, c => c.Value);

                if (claimsDict.ContainsKey(claimKey))
                {
                    return claimsDict[claimKey];
                }
                else
                {
                    return null;
                }
            }
            catch
            {
                return null;
            }
        }

        private static TokenInfo FindTokenInfo(string tokenKey, string token, TokenInfoRepository tokenRepository)
        {
            TokenInfo tokenValue = null;
            var retryCount = 0;
            while (retryCount < 3)
            {
                try
                {
                    tokenValue = tokenRepository.FindTokenInfoByKey(tokenKey);
                    break;
                }
                catch (Exception)
                {
                    return null;
                    Thread.Sleep(TokenRetryPauseInSeconds * 1000);
                    retryCount++;
                }
            }

            return tokenValue;
        }

        private static IEnumerable<Claim> ValidateLoginToken(string token)
        {
            var configuration = ServiceLocator.Resolve<IConfiguration>();
            var httpContext = ServiceLocator.Resolve<IHttpContextAccessor>();
            var tokenHandler = new JwtSecurityTokenHandler();
            var issuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Jwt:Secret"]));
            var validationParameters = new TokenValidationParameters()
            {
                ValidAudience = configuration["Jwt:Audience"],
                IssuerSigningKey = issuerSigningKey,
                ValidIssuer = configuration["Jwt:Issuer"],
                RequireExpirationTime = false,
                ValidateLifetime = false,
                ValidateAudience = false,
                ValidateIssuer = false,
                ValidateIssuerSigningKey = true,
            };
            try
            {
                var principal = tokenHandler.ValidateToken(token, validationParameters, out SecurityToken securityToken);
                var claims = principal.Claims.ToDictionary(c => c.Type, d => d.Value);

                if (!claims.ContainsKey("Name"))
                {
                    return null;
                }

                var tokenKey = GetTokenKey(claims["Name"], claims["Prefix"]);
                var tokenRepository = new TokenInfoRepository(ServiceLocator.Resolve<ApiDBContent>(), ServiceLocator.Resolve<IDaemonDistributedCache>());
                var isSupport = SupportModeHelper.IsSupportModeUser(claims["Name"]);
                var preTokenValue = FindTokenInfo(tokenKey, token, tokenRepository);
                if (preTokenValue == null || !string.Equals(preTokenValue.TokenValue, token))
                {
                    // force getting TokenInfo from database instead of cache with explicit token value
                    var databaseToken = tokenRepository.FindTokenInfoByKeyAndValue(tokenKey, token);
                    if (databaseToken != null)
                    {
                        // database token matched request, cache is out of sync so force backfill via save
                        // this can happen due to Redis replication lag or user signing in during outage
                        tokenRepository.Save(databaseToken);
                    }
                    else
                    {
                        return null;
                    }
                }

                if (preTokenValue.Expiration > DateTime.Now)
                {
                    var headers = httpContext.HttpContext.Request?.Headers;
                    if (headers == null
                        || !headers.TryGetValue("no-extend-expiration", out StringValues noExtendExpirationValues)
                        || !noExtendExpirationValues.Any()
                        || !bool.TryParse(noExtendExpirationValues.FirstOrDefault(), out bool noExtendExpiration)
                        || !noExtendExpiration)
                    {
                        try
                        {
                            // refresh the expiration for current token.
                            preTokenValue.Expiration = GetExpiration();
                            SaveToken(isSupport, preTokenValue);
                        }
                        catch (Exception)
                        {
                        }
                    }

                    return principal.Claims;
                }

                ServiceLocator.Resolve<SessionItemRepository>().DeleteUserSession(Convert.ToInt32(claims["Id"] ?? "-1"), LogoutReasonEnum.SessionTimeout);

                if (isSupport)
                {
                    tokenRepository.Delete(tokenKey, token);
                }
                else
                {
                    tokenRepository.Delete(tokenKey);
                }

                return null;
            }
            catch (Exception ex)
            {
                return null;
            }
        }

        internal static string GetTokenKey(string userName, string prefix)
        {
            return $"{prefix}_{userName}";
        }

        internal static IEnumerable<Claim> ValidateToken(string token)
        {
            if (string.IsNullOrEmpty(token))
            {
                var requestUrl = ServiceLocator.Resolve<IHttpContextAccessor>().HttpContext?.Request?.Path;
                var urlInfo = requestUrl != null ? requestUrl.ToString() : " from core service";
                return null;
            }

            try
            {
                var claims = GetAllClaims(token);
                return ValidateLoginToken(token);
            }
            catch (Exception)
            {
            }
            return null;
        }

        internal static int GetExpirationMinutes()
        {
            var context = ServiceLocator.Resolve<ApiDBContent>();
            int? sessionTimeout = context.ClientConfig.FirstOrDefault()?.SessionTimeout;
            return sessionTimeout.Value;
        }

        internal static IEnumerable<Claim> GetAllClaims(string token)
        {
            try
            {
                var tokenHandler = new JwtSecurityTokenHandler();
                var jwtToken = (JwtSecurityToken)tokenHandler.ReadToken(token);
                return jwtToken.Claims;
            }
            catch
            {
                return new List<Claim>();
            }
        }
    }
}